<?php
/*
  TODO:

  - Dynamic select will need a new api

*/
require_once ('class_mysql_list.php');
require_once ('class_tree_select_element.php');

abstract class FormElement
{
  const CSS_PREFIX = 'form';
  const CSS_SEP = '-';
  const CSS_TYPE = 'type';
  const CSS_CONTAINER = 'container';
  const CSS_LABEL = 'label';
  const CSS_ELEMENT = 'element';
  const CSS_ERROR = 'error';
  const CSS_HIDDEN = 'hidden';
  const CSS_MESSAGE = 'message';
  const CSS_BOTTOM = 'bottom';

  const ELEMENT_POSITION_BOTTOM = 'bottom';
  const ELEMENT_POSITION_RIGHT = 'right';

  const HINT_MODE_AUTO = 0;
  const HINT_MODE_MANUAL = 1;

  const TEXT_REQUIRED_FIELD = 'This is a required field';

  protected $name;
  protected $value;
  protected $defaultValue;
  protected $hint;
  protected $hintMode = self::HINT_MODE_MANUAL;
  protected $hidden = false;
  protected $required = true;
  protected $autoFocus = false;
  protected $readOnly = false;
  protected $disabled = false;
  protected $label;
  protected $labelVisible = true;
  protected $elementPosition = self::ELEMENT_POSITION_RIGHT;
  protected $valid = true;
  protected $validationRules = array();
  protected $cssClasses = array();
  protected $modifiers = array();

  /* An array of error messages associated with this element */
  protected $errors = array();
  protected $hasError = false;

  protected $validElementPositions = array(
    self::ELEMENT_POSITION_RIGHT,
    self::ELEMENT_POSITION_BOTTOM
  );

  protected $validHintModes = array(
    self::HINT_MODE_AUTO,
    self::HINT_MODE_MANUAL
  );

  /* Constructor */
  public function __construct($name, $label = NULL)
  {
    $this->setName($name);
    $this->setId(self::CSS_PREFIX . self::CSS_SEP . $this->getName());
    if ($label == NULL) {
      $this->setLabel(ucwords(str_replace('_', ' ', $name)));
    } else {
      $this->setLabel($label);
    }
  }

  /* Display the form element */
  public function display()
  {
    /*
      If the form has been submitted, but there are errors, we want to show the
      current value. If there is no current value, i.e. the form has not been
      submitted, we want to show the default value
    */
    if (is_null($this->getValue()) && !is_null($this->getDefaultValue())) {
      $this->setValue($this->getDefaultValue());
    }

    $containerClass = implode(self::CSS_SEP, array(self::CSS_PREFIX,
        self::CSS_ELEMENT, self::CSS_CONTAINER));
    if ($this->hidden) {
      $containerClass .= self::CSS_SEP . self::CSS_HIDDEN;
    } elseif ($this->hasError) {
      $containerClass .= self::CSS_SEP . self::CSS_ERROR;
    }
    $containerId = $this->id . self::CSS_SEP . self::CSS_CONTAINER;
    echo "<div id=\"$containerId\" class=\"$containerClass\">\n";
    if ($this->labelVisible) {
      $this->displayLabel();
    }
    $this->displayElementContainer();

    $this->displayJavaScriptContainer();
    $this->displayErrors();
    echo "</div>\n\n";
  }

  protected function displayElementContainer()
  {
    $elementClass = self::CSS_PREFIX . self::CSS_SEP . self::CSS_ELEMENT .
        (($this->hasError) ? self::CSS_SEP . self::CSS_ERROR : '');
    if ($this->elementPosition == self::ELEMENT_POSITION_BOTTOM) {
      $elementClass .= ' ' . implode(self::CSS_SEP, array(self::CSS_PREFIX,
        self::CSS_ELEMENT, self::CSS_BOTTOM));
    }
    echo "<div class=\"$elementClass\">\n";
    $this->displayElement();
    echo "</div>\n";
  }

  protected function displayJavaScriptContainer()
  {
    echo "<script type=\"text/javascript\">\n";
    $this->displayJavascript();
    echo "</script>\n";
  }

  protected function displayLabel()
  {
    $labelClass = self::CSS_PREFIX . self::CSS_SEP . self::CSS_LABEL .
        (($this->hasError) ? self::CSS_SEP . self::CSS_ERROR : '');
    $labelId = $this->id . self::CSS_SEP . self::CSS_LABEL;
    $asterisk = ($this->required) ? '* ' : '';
    echo "<div class=\"$labelClass\">$asterisk";
    echo "<span id=\"$labelId\">$this->label</span></div>\n";
  }

  protected function displayErrors()
  {
    if ($this->hasError) {
      $containerClass = self::CSS_PREFIX . self::CSS_SEP . self::CSS_ERROR .
          self::CSS_SEP . self::CSS_CONTAINER;
      $messageClass = self::CSS_PREFIX . self::CSS_SEP . self::CSS_ERROR .
          self::CSS_SEP . self::CSS_MESSAGE;
      echo "<div class=\"$containerClass\">\n";
      foreach ($this->errors AS $error) {
        echo "<div class=\"$messageClass\">\n";
        echo $error;
        echo "</div>\n";
      }
      echo "</div>\n";
    }
  }

  protected function displayJavaScript()
  {
    echo "var element = new FormElement('$this->name');\n";
    if ($this->autoFocus) {
      echo "element.setAutoFocus(true);\n";
    }
    if (isset($this->hint)) {
      echo "element.setHint('$this->hint', $this->hintMode);\n";
    }
    echo "formSystem.addElement(element);\n";
  }

  // TODO: Need to figure out what to do here for multiple html elements.
  public function setAttribute($name, $value)
  {
    $this->attributes[$name] = $value;
  }

  /**
   * FormElement::getFormElementHTML()
   *
   * Convert all applicable properties of this element into an html string.
   * These are all the attributes of the tag which is used for the form element
   * itself. (i.e. input, select, checkbox, etc)
   *
   * @return void
   */
  protected function getFormElementHTML($tagName, $close = true)
  {
    $html = '<' . $tagName;
    foreach ($this->attributes as $name => $value) {
      if (isset($value)) {
        $html .= " $name=\"$value\"";
      }
    }
    $baseFormElementClass = self::CSS_PREFIX . self::CSS_SEP . self::CSS_TYPE .
        self::CSS_SEP . $this->type;
    array_unshift($this->cssClasses, $baseFormElementClass);
    $formElementClasses = implode(' ', $this->cssClasses);
    $html .= " class=\"$formElementClasses\"";
    return $html . (($close) ? ' /' : '') . '>' . "\n";
  }

  abstract function displayElement();

  // ---------------------------------------------------------------------------
  // Getters and Setters
  // ---------------------------------------------------------------------------

  public function setId($id)
  {
    $this->id = $id;
    $this->setAttribute('id', $this->id);
  }

  public function getid()
  {
    return $this->id;
  }

  public function setName($name)
  {
    $this->name = $name;
    $this->setAttribute('name', $this->name);
  }

  public function getName()
  {
    return $this->name;
  }

  public function setValue($value)
  {
    $this->value = $value;
    $this->setAttribute('value', $this->value);
  }

  public function modifyValue()
  {
    foreach ($this->modifiers as $modifier) {
      $this->value = call_user_func($modifier, $this->value);
    }
  }

  public function getValue()
  {
    return $this->value;
  }

  public function setDefaultValue($defaultValue)
  {
    $this->defaultValue = $defaultValue;
  }

  public function getDefaultValue()
  {
    return $this->defaultValue;
  }

  public function setHint($hint)
  {
    $this->hint = htmlentities($hint, ENT_QUOTES);
  }

  public function getHint()
  {
    return $this->hint;
  }

  public function setHintMode($hintMode)
  {
    if (in_array($hintMode, $this->validHintModes)) {
      $this->hintMode = $hintMode;
    } else {
      throw new InvalidArgumentException('Invalid hint mode specified');
    }
  }

  public function getHintMode()
  {
    return $this->hintMode;
  }

  public function setRequired($required)
  {
    if (is_bool($required)) {
      $this->required = $required;
    } else {
      throw new InvalidArgumentException('Boolean expected');
    }
  }

  public function getRequired()
  {
    return $this->required;
  }

  public function setDisabled($disabled)
  {
    if (is_bool($disabled)) {
      $this->disabled = $disabled;
      if ($this->disabled) {
        $this->setAttribute('disabled', 'disabled');
      } else {
        $this->setAttribute('disabled', NULL);
      }
    } else {
      throw new InvalidArgumentException('Boolean expected');
    }
  }

  public function getDisabled()
  {
    return $this->disabled;
  }

  public function setReadOnly($readOnly)
  {
    if (is_bool($readOnly)) {
      $this->readOnly = $readOnly;
      if ($this->readOnly) {
        $this->setAttribute('readonly', 'readonly');
      } else {
        $this->setAttribute('readonly', NULL);
      }
    } else {
      throw new InvalidArgumentException('Boolean expected');
    }
  }

  public function getReadOnly()
  {
    return $this->readOnly;
  }

  public function setLabel($label)
  {
    $this->label = $label;
  }

  public function getLabel()
  {
    return $this->label;
  }

  public function setElementPosition($position)
  {
    if (in_array($position, $this->validElementPositions)) {
      $this->elementPosition = $position;
    } else {
      throw new InvalidArgumentException('Invalid element position');
    }
  }

  public function getElementPosition()
  {
    return $this->elementPosition;
  }

  public function setHidden($hidden)
  {
    if (is_bool($hidden)) {
      $this->hidden = $hidden;
    } else {
      throw new InvalidArgumentException('Boolean expected');
    }
  }

  public function getHidden()
  {
    return $this->hidden;
  }

  public function setAutoFocus($autoFocus)
  {
    if (is_bool($autoFocus)) {
      $this->autoFocus = $autoFocus;
    } else {
      throw new InvalidArgumentException('Boolean expected');
    }
  }

  public function getAutoFocus()
  {
    return $this->autoFocus;
  }

  public function setLabelVisibility($visible)
  {
    $this->labelVisible = $visible;
  }

  public function getLabelVisibility()
  {
    return $this->labelVisible;
  }

  public function addCssClass($className)
  {
    $this->cssClasses[] = $className;
  }

  public function convert($type)
  {
    $element = new $type($this->getName());
    $element->setLabel($this->getLabel());
    if (isset($this->options) && isset($element->options)) {
      $element->setOptions($this->getOptions());
    }
    return $element;
  }

  // ---------------------------------------------------------------------------
  // Validation
  // ---------------------------------------------------------------------------

  public function addValidationRule(ValidationRule $rule)
  {
    $this->validationRules[] = $rule;
  }

  public function validate()
  {
    $this->modifyValue();
    if (strlen($this->getValue()) == 0 && $this->required) {
      $this->addError(self::TEXT_REQUIRED_FIELD);
      $this->valid = false;
    }
    foreach ($this->validationRules as $rule) {
      $ruleValid = $rule->validate($this->getValue());
      $this->valid &= $ruleValid;
      if (!$ruleValid) {
        $this->addError($rule->getMessage());
      }
    }
    return $this->valid;
  }

  // ---------------------------------------------------------------------------
  // Miscellaneous
  // ---------------------------------------------------------------------------

  public function addModifier($modifier)
  {
    $this->modifiers[] = $modifier;
  }

  public function addError($message)
  {
    $this->errors[] = $message;
    $this->valid = false;
    $this->hasError = true;
  }

  /*
    Same as setting the value, only this is coming from the GET or POST values
    generated by the form element's html elements. This is necessary because
    some form elements require more than one html element. $data contains all
    the POST or GET values in an associative array. This method pulls the value
    for the element out of this array.
  */
  public function loadData($data)
  {
    if (isset($data[$this->name])) {
      $this->setValue($data[$this->name]);
    }
  }
}

abstract class MultiOptionFormElement extends FormElement
{
  protected $options;

  public function __construct($name, $label = NULL, $options = array())
  {
    parent::__construct($name, $label);
    $this->setOptions($options);
  }

  /**
   * MultiOptionFormElement::setOptions()
   *
   * Convert numeric arrays to associative arrays with key == value. This allows
   * us to treat the options as an associative array everywhere else.    
   *
   * @param mixed $options
   * @return void
   */
  public function setOptions(array $options)
  {
    if (array_keys($options) !== range(0, sizeof($options) - 1)) {
      $this->options = $options;
    } else {
      $this->options = array();
      foreach ($options as $option) {
        $this->options[$option] = $option;
      }
    }
  }

  // TODO: Get rid of this and the dependency on MySQLList.
  public function setOptionsFromMysql(MySQLList $list)
  {
    $this->options = $list->getArray();
  }
}

class TextElement extends FormElement
{
  protected $type = 'text';

  public function displayElement()
  {
    $this->setAttribute('type', 'text');
    echo $this->getFormElementHTML('input');
  }
}

class HiddenElement extends FormElement
{
  protected $type = 'hidden';

  public function __construct($value, $label = NULL)
  {
    parent::__construct($value, $label);
    $this->hidden = true;
  }

  public function displayElement()
  {
    $this->setAttribute('type', 'hidden');
    echo $this->getFormElementHTML('input');
  }
}

class PasswordElement extends FormElement
{
  protected $type = 'password';

  public function displayElement()
  {
    $this->setAttribute('type', 'password');
    echo $this->getFormElementHTML('input');
  }
}

class SelectElement extends MultiOptionFormElement
{
  protected $type = 'select';

  public function displayElement()
  {
    // TODO: Reimplement dynamic options.
    $this->setAttribute('value', null);
    echo $this->getFormElementHTML('select', false);

    /*
      If a value hasn't been set, or if it is not required that a value be set,
      create a blank options.
    */
    if (!$this->getValue() && !$this->required) {
      echo "<option value=\"\"></option>\n";
    }

    foreach ($this->options as $key => $value) {
      $selected = ($this->hasValue($key)) ? ' selected="selected"' : '';
      echo "<option value=\"$key\"$selected>$value</option>\n";
    }
    echo "</select>\n";
  }

  protected function hasValue($value)
  {
    return ($value == $this->getValue());
  }

  /*
    Override the default since there is no circumstance where a select box needs
    to show an asterisk.
  */
  protected function displayLabel()
  {
    $labelClass = self::CSS_PREFIX . self::CSS_SEP . self::CSS_LABEL .
        (($this->hasError) ? self::CSS_SEP . self::CSS_ERROR : '');
    $labelId = $this->id . self::CSS_SEP . self::CSS_LABEL;
    echo "<div class=\"$labelClass\">";
    echo "<span id=\"$labelId\">$this->label</span></div>\n";
  }
}

class MultiSelectElement extends SelectElement
{
  protected $type = 'multiselect';
  protected $value;

  public function getValue()
  {
    if (!is_null($this->value)) {
      return implode(',', $this->value);
    }
    return NULL;
  }

  public function setValue($value)
  {
    if (is_array($value)) {
      $this->value = $value;
    } else {
      $this->value = explode(',', $value);
    }
  }

  public function getDefaultValue()
  {
    if (!is_null($this->defaultValue)) {
      return implode(',', $this->defaultValue);
    }
    return NULL;
  }

  public function setDefaultValue($value)
  {
    if (is_array($value)) {
      $this->defaultValue = $value;
    } else {
      $this->defaultValue = explode(',', $value);
    }
  }

  protected function hasValue($value)
  {
    if (is_null($this->value)) {
      return false;
    }
    return in_array($value, $this->value);
  }

  public function setName($name)
  {
    $this->name = $name;
    $this->setAttribute('name', $name . '[]');
  }

  public function displayElement()
  {
    $this->setAttribute('multiple', 'multiple');
    parent::displayElement();
  }

  public function validate()
  {
    $this->modifyValue();
    if (count($this->value) == 0 && $this->required) {
      $this->addError(self::TEXT_REQUIRED_FIELD);
    }
    foreach ($this->validationRules as $rule) {
      $ruleValid = $rule->validate($this->value);
      $this->valid &= $ruleValid;
      if (!$ruleValid) {
        $this->addError($rule->getMessage());
      }
    }
    return $this->valid;
  }
}

class DateElement extends FormElement
{
  protected $type = 'date';
  protected $minDate;
  protected $maxDate;
  protected $highlightDate;

  public function displayElement()
  {
    echo $this->getFormElementHTML('input');
  }

  protected function displayJavaScript()
  {
    echo "var element = new DateElement('$this->name');\n";
    if ($this->autoFocus) {
      echo "element.setAutoFocus(true);\n";
    }
    if (isset($this->hint)) {
      echo "element.setHint('$this->hint', $this->hintMode);\n";
    }
    echo "formSystem.addElement(element);\n";
  }

  public function setMinDate($minDate)
  {
    // TODO: Validate dates
    $this->minDate = $minDate;
  }

  public function getMinDate()
  {
    return $this->minDate;
  }

  public function setMaxDate($maxDate)
  {
    $this->maxDate = $maxDate;
  }

  public function getMaxDate()
  {
    return $this->maxDate;
  }

  public function setHighlightDate($highlightDate)
  {
    $this->highlightDate = $highlightDate;
  }

  public function getHighlightDate()
  {
    return $this->highlightDate;
  }
}

class TextareaElement extends FormElement
{
  protected $type = 'textarea';

  public function __construct($name, $label = NULL)
  {
    parent::__construct($name, $label);
    /* These numbers (4, 20) are arbitary inserted for XHTML compliance */
    $this->setAttribute('rows', '4');
    $this->setAttribute('cols', '20');
  }

  public function displayElement()
  {
    echo $this->getFormElementHTML('textarea', false);
    echo htmlentities($this->value);
    echo "</textarea>\n";
  }

  public function setValue($value)
  {
    $this->value = $value;
    /* Note that we don't want to set the value attribute for a textarea */
  }
}

class ButtonElement extends FormElement
{
  protected $type = 'button';

  // TODO: We're going to need to allow the user to set events.
  public function __construct($name, $label = NULL)
  {
    parent::__construct($name, $label);
    $this->labelVisible = false;
  }

  public function displayElement()
  {
    $this->setAttribute('type', 'button');
    $this->setAttribute('value', $this->label);
    echo $this->getFormElementHTML('input');
  }
}

class CheckboxElement extends FormElement
{
  protected $type = 'checkbox';
  protected $option;

  public function __construct($name, $label = NULL, $option = '1')
  {
    parent::__construct($name, $label);
    $this->option = $option;
    $this->setRequired(false);
    $this->setAttribute('type', 'checkbox');
    $this->setAttribute('value', $option);
  }

  public function displayElement()
  {
    if ($this->value == $this->option) {
      $this->setAttribute('checked', 'checked');
    }
    echo $this->getFormElementHTML('input');
  }

  protected function displayJavaScript()
  {
    echo "var element = new CheckboxElement('$this->name');\n";
    if ($this->autoFocus) {
      echo "element.setAutoFocus(true);\n";
    }
    if (isset($this->hint)) {
      echo "element.setHint('$this->hint', $this->hintMode);\n";
    }
    echo "formSystem.addElement(element);\n";
  }
}

class RadioButtonsElement extends MultiOptionFormElement
{
  protected $type = 'radio';
  protected $columns = 1;

  public function __construct($name, $label = NULL, $options = array())
  {
    parent::__construct($name, $label, $options);
    $this->setRequired(false);
    if (count($this->options) >= 20) {
      $this->columns = 4;
    } elseif (count($this->options) >= 12) {
      $this->columns = 3;
    } elseif (count($this->options) >= 6) {
      $this->columns = 2;
    }
  }

  public function setOptions(array $options)
  {
    parent::setOptions($options);
    /* One of the radio buttons *must* be selected by default */
    if (count($this->options) > 0) {
      $this->setDefaultValue(key($this->options));
    }
  }

  public function setColumns($columns)
  {
    if (is_numeric($columns)) {
      $this->columns = $columns;
    } else {
      throw new InvalidArgumentException('Number expected');
    }
  }

  public function displayElement()
  {
    $this->setAttribute('type', $this->type);
    echo "<table cellpadding=\"0px\" cellspacing=\"0px\">\n";
    $i = 0;
    foreach ($this->options as $value => $label) {
      if ($i % $this->columns == 0) {
        echo "<tr>";
      }
      $this->setAttribute('value', $value);
      $this->setAttribute('id', self::CSS_PREFIX . $this->getName() .
          '-' . $i);
      if ($this->hasValue($value)) {
        $this->setAttribute('checked', 'checked');
      } else {
        $this->setAttribute('checked', NULL);
      }
      echo '<td><label>';
      echo $this->getFormElementHTML('input');
      echo "$label</label></td>";
      $i++;
      if ($i % $this->columns == 0) {
        echo "</tr>\n";
      }
    }
    echo "</table>\n";
  }

  protected function hasValue($value)
  {
    return ($this->value == $value);
  }
}

class CheckboxesElement extends RadioButtonsElement
{
  protected $type = 'checkbox';
  protected $value = array();
  protected $defaultValue = array();

  public function getValue()
  {
    return implode(',', $this->value);
  }

  public function setValue($value)
  {
    if (is_array($value)) {
      $this->value = $value;
    } else {
      $this->value = explode(',', $value);
    }
  }

  public function getDefaultValue()
  {
    return implode(',', $this->defaultValue);
  }

  public function setDefaultValue($value)
  {
    if (is_array($value)) {
      $this->defaultValue = $value;
    } else {
      $this->defaultValue = explode(',', $value);
    }
  }

  protected function hasValue($value)
  {
    return in_array($value, $this->value);
  }

  public function setOptions(array $options)
  {
    MultiOptionFormElement::setOptions($options);
  }

  public function setName($name)
  {
    $this->name = $name;
    $this->setAttribute('name', $name . '[]');
  }

  public function validate()
  {
    if (count($this->value) == 0 && $this->required) {
      $this->addError(self::TEXT_REQUIRED_FIELD);
    }
    foreach ($this->validationRules as $rule) {
      $ruleValid = $rule->validate($this->value);
      $this->valid &= $ruleValid;
      if (!$ruleValid) {
        $this->addError($rule->getMessage());
      }
    }
    return $this->valid;
  }
}

class AutoCompleteElement extends MultiOptionFormElement
{
  protected $showKey = true;
  protected $type = 'autocomplete';

  public function displayElement()
  {
    echo $this->getFormElementHTML('input');
  }

  public function setShowKey($showKey)
  {
    if (is_bool($showKey)) {
      $this->showKey = $showKey;
    } else {
      throw new InvalidArgumentException('Boolean expected');
    }
  }

  protected function displayJavaScript()
  {
    echo "var element = new AutoCompleteElement('$this->name');\n";
    if ($this->autoFocus) {
      echo "element.setAutoFocus(true);\n";
    }
    if (isset($this->hint)) {
      echo "element.setHint('$this->hint', $this->hintMode);\n";
    }
    if (!$this->showKey) {
      echo "element.autoComplete.prefs.showKey = false;\n";
    }
    echo "element.setOptions([";
    $javascriptOptions = array();
    foreach ($this->options as $value => $label) {
      $value = htmlentities($value, ENT_QUOTES);
      $label = htmlentities($label, ENT_QUOTES);
      $javascriptOptions[] = "['$value', '$label']";
    }
    echo implode(',', $javascriptOptions);
    echo "]);\n";
    echo "formSystem.addElement(element);\n";
  }
}

class WysiwygElement extends TextareaElement
{
  const MODE_BASIC = 0;
  const MODE_FULL = 1;

  protected $type = 'wysiwyg';
  protected $mode = self::MODE_BASIC;

  protected $validModes = array(
    self::MODE_BASIC,
    self::MODE_FULL
  );

  public function setMode($mode)
  {
    if (!in_array($mode, $this->validModes)) {
      throw new InvalidArgumentException("Specified mode ($mode) is invalid");
    }
    $this->mode = $mode;
  }

  protected function displayJavaScript()
  {
    $stylesheet = HTTP_INCLUDES . 'style-main.css';
    echo "var element = new WysiwygElement('$this->name', '$stylesheet', " .
        "$this->mode);\n";
    if ($this->autoFocus) {
      echo "element.setAutoFocus(true);\n";
    }
    if (isset($this->hint)) {
      echo "element.setHint('$this->hint', $this->hintMode);\n";
    }
    echo "formSystem.addElement(element);\n";
  }
}

class TimeElement extends FormElement
{
  protected $type = 'time';
  protected $hour;
  protected $minute;
  protected $ampm;

  // TODO: Properly implement seconds.

  public function displayElement()
  {
    $this->setAttribute('type', 'text');
    $hour = new SelectElement($this->name);
    $hour->setOptions(range(1, 12));
    $hour->addCssClass('form-element-time-hour');
    if ($this->hour) {
      $hour->setValue($this->hour);
    }
    $hour->displayElement();

    $minute = new SelectElement($this->name . '-minute');
    $minutes = range(0, 55, 5);
    $minutes[0] = '00';
    $minutes[1] = '05';
    $minute->setOptions($minutes);
    $minute->addCssClass('form-element-time-minute');
    if ($this->minute) {
      $minute->setValue($this->minute);
    }
    $minute->displayElement();

    $ampm = new SelectElement($this->name . '-ampm');
    $ampm->setOptions(array('AM', 'PM'));
    $ampm->addCssClass('form-element-time-ampm');
    if ($this->ampm) {
      $ampm->setValue($this->ampm);
    }
    $ampm->displayElement();
  }

  public function loadData(array $data)
  {
    if (isset($data[$this->name])) {
      $this->hour = $data[$this->name];
    } else {
      throw new LogicException("Element $this->name failed to load data");
    }
    if (isset($data[$this->name . '-minute'])) {
      $this->minute = $data[$this->name . '-minute'];
    } else {
      throw new LogicException("Element $this->name failed to load data");
    }
    if (isset($data[$this->name . '-ampm'])) {
      $this->ampm = $data[$this->name . '-ampm'];
    } else {
      throw new LogicException("Element $this->name failed to load data");
    }
    $this->value = $this->hour . ':' . $this->minute . ':00';
  }

  public function setValue($value)
  {
    $parts = explode(':', $value);
    if (count($parts) !== 3) {
      throw new InvalidArgumentException('Value supplied is not a valid time');
    }
    if ($parts[0] < 0 || $parts[0] > 24 || !is_numeric($parts[0])) {
      throw new InvalidArgumentException('Value supplied is not a valid time');
    }
    if ($parts[1] < 0 || $parts[1] > 59 || !is_numeric($parts[1])) {
      throw new InvalidArgumentException('Value supplied is not a valid time');
    }
    if ($parts[2] < 0 || $parts[2] > 59 || !is_numeric($parts[2])) {
      throw new InvalidArgumentException('Value supplied is not a valid time');
    }
    if ($parts[0] > 12) {
      $this->hour = $parts[0] - 12;
      $this->ampm = 'PM';
    } else {
      $this->hour = $parts[0];
      $this->ampm = 'AM';
    }
    $this->minute = $parts[1];
    $this->value = $value;
  }

  public function getValue()
  {
    if ($this->value) {
      return $this->hour . ':' . $this->minute . ':00';
    } else {
      return;
    }
  }
}

class ListElement extends FormElement
{
  protected $type = 'hidden';
  protected $options;

  public function displayElement()
  {
    if (isset($this->options)) {
      $element = new SelectElement($this->name . '-entry');
      $element->setOptions($this->options);
      $element->addCssClass('form-element-list-entry');
      $element->displayElement();
    } else {
      $element = new TextElement($this->name . '-entry');
      $element->addCssClass('form-element-list-entry');
      $element->displayElement();
    }

    $element = new ButtonElement($this->name . '-add', 'Add');
    $element->addCssClass('form-element-list-add');
    $element->displayElement();

    $element = new MultiSelectElement($this->name . '-list');
    $element->addCssClass('form-element-list-list');
    $element->setOptions(explode(',', $this->getValue()));
    $element->displayElement();

    $element = new ButtonElement($this->name . '-delete', 'Del');
    $element->addCssClass('form-element-list-delete');
    $element->displayElement();

    $this->setAttribute('type', 'hidden');
    echo $this->getFormElementHTML('input');
  }

  public function setOptions($options)
  {
    $this->options = $options;
  }

  protected function displayJavaScript()
  {
    $addElementId = $this->id . '-add';
    $entryElementId = $this->id . '-entry';
    $listElementId = $this->id . '-list';
    $deleteElementId = $this->id . '-delete';

    $visibleElementIds = "['" . implode("','", array($addElementId,
        $entryElementId, $deleteElementId, $listElementId)) . "']";
    echo "var element = new ListElement('$this->name', '$this->id',
        '$entryElementId', $visibleElementIds);\n";
    if ($this->autoFocus) {
      echo "element.setAutoFocus(true);\n";
    }
    if (isset($this->hint)) {
      echo "element.setHint('$this->hint', $this->hintMode);\n";
    }
    echo "formSystem.addElement(element);\n";
  }

  public function validate()
  {
    $this->modifyValue();
    if (strlen($this->getValue()) == 0 && $this->required) {
      $this->addError(self::TEXT_REQUIRED_FIELD);
      $this->valid = false;
    }
    foreach ($this->validationRules as $rule) {
      $values = explode(',', $this->getValue());
      foreach ($values as $value) {
        $ruleValid = $rule->validate($value);
        $this->valid &= $ruleValid;
      }
      if (!$ruleValid) {
        $this->addError($rule->getMessage());
      }
    }
    return $this->valid;
  }

  public function modifyValue()
  {
    foreach ($this->modifiers as $modifier) {
      $values = explode(',', $this->getValue());
      foreach ($values as &$value) {
        $value = call_user_func($modifier, $value);
      }
      $this->setValue(implode(',', $values));
    }
  }
}

?>